// Copyright 2011 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


/**
 * @fileoverview Helper functions for module creation.  To utilize this code
 * requires the use of the externs.js file when compiling a binary.  The
 * externs file is located at
 *
 *     //testing/chronos/shell_extension/builddefs:externs.js
 *
 * A namespace is considered a hierarchy of objects the terminate in a
 * objects or functions.  If a function is reached, the recursion does not
 * continue.
 *
 * Public properties:
 *   addModuleData(disable, enable, getName, install, uninstall) - Optionally
 *       takes each module function and creates an new object with those
 *       functions.  The data is added to the global module management data.
 *       Functions can be null if not set.
 *   addNamespace(scrObj, target) - Merge the source namespace into the target
 *       namespace by adding any missing subtrees, merging common objects, and
 *       throwing an exception if source namespace wants to merge with a
 *       non-object value in the target.
 *
 * @author jasonstredwick@google.com (Jason Stredwick)
 */


goog.provide('ModuleHelper');


/**
 * Generates an object containing the required module functions and adding it
 * the global set of functions.  Functions can be null if not set.
 * @param {?function()} disable The disable function.
 * @param {?function()} enable The enable function.
 * @param {?function()} getName The getName function.
 * @param {?function()} install The install function.
 * @param {?function()} uninstall The uninstall function.
 */
ModuleHelper.addModuleData = function(disable, enable, getName, install,
                                      uninstall) {
  console = console || {};
  console.log = console.log || function() {};
  console.error = console.error || function() {};

  // Validate manipulator functions.
  if (typeof(getName) != 'function') {
    console.error('Missing getName function; skipping module.');
    return;
  } else if (typeof(disable) != 'function' ||
             typeof(enable) != 'function' ||
             typeof(install) != 'function' ||
             typeof(uninstall) != 'function') {
    console.error('Missing manipulator functions. Skipping module ' +
                  getName() + '.');
    return;
  }

  // Create module management data objects if necessary.
  if (typeof(moduleObjs) == 'undefined') {
    moduleObjs = {};
  }

  if (typeof(moduleObjsOrder) == 'undefined') {
    moduleObjsOrder = [];
  }

  // Validate module management data.
  if (!moduleObjs || typeof(moduleObjs) != 'object' ||
      !moduleObjsOrder || typeof(moduleObjsOrder) != 'object') {
    console.error('Module environment invalid. Skipping module ' + getName() +
                  '.');
    return;
  }

  // Check if module already loaded.
  if (getName() && getName() in moduleObjs) {
    console.log('Module (' + getName() + ') already loaded; ignoring.');
    return;
  }

  var doNothing = function() {};
  var obj = {};
  obj['disable'] = disable ? disable : doNothing;
  obj['enable'] = enable ? enable : doNothing;
  obj['getName'] = getName ? getName : doNothing;
  obj['install'] = install ? install : doNothing;
  obj['uninstall'] = uninstall ? uninstall : doNothing;

  moduleObjs[getName()] = obj;
  moduleObjsOrder.push(getName());

  console.log('Module[' + (moduleObjsOrder.length - 1) + ']: "' + getName() +
              '" loaded.');
};


/**
 * Merge the source namespace into the target namespace by adding any missing
 * subtrees, merging common objects, and throwing an exception if source
 * namespace wants to merge with a non-object value in the target.
 * @param {!Object|Function} srcObj The object to extract the namespace from.
 * @param {!Object|Function} target The target object to add the namespace to.
 */
ModuleHelper.addNamespace = function(srcObj, target) {
  ModuleHelper.addNamespace_(srcObj, target);
};


/**
 * See ModuleHelper.addNamespace.
 * @param {!Object|Function} srcObj The object to extract the namespace from.
 * @param {!Object|Function} target The target object to add the namespace to.
 * @param {string=} opt_curName Used to track the namespace as it recurses.
 * @private
 */
ModuleHelper.addNamespace_ = function(srcObj, target, opt_curName) {
  var currentNamespace = opt_curName || '{target}';

  // Validate inputs
  if (!srcObj || typeof(srcObj) != 'object') {
    throw 'Invalid namespace (' + currentNamespace + '); source is not an ' +
          'object.';
  } else if (!target || typeof(target) != 'object') {
    throw 'Invalid namespace (' + currentNamespace + '); target is not an ' +
          'object.';
  }

  // Traverse keys in the source object and add the key/value pairs that don't
  // already exist and recursively merge the rest.
  for (var key in srcObj) {
    if (key in target) {
      var nextName = currentNamespace + '.' + key;
        var targetValue = target[key];

      // Current namespace and new namespace can only share objects in common.
      if (!targetValue || typeof(targetValue) != 'object') {
        throw 'Invalid namespace; non-object value (' + typeof(targetValue) +
              ') already exists in ' + nextName;
      }

      // Recurse into the next level of the namespace for both target and
      // srcObj.
      ModuleHelper.addNamespace_(srcObj[key], target[key], nextName);
    } else {
      // The target namespace does not contain this subtree, so add the whole
      // subtree to the target.
      target[key] = srcObj[key];
    }
  }
};

